import { isNull } from '@gowiny/js-utils'
import {
    ref,
    ComponentPublicInstance,
    ComponentOptions,
    VNode,
    PropType,
    VNodeProps,
    AllowedComponentProps,
    ComponentCustomProps,
    Prop,
    ComponentObjectPropsOptions,
    EmitsOptions,
    shallowRef,
    shallowReactive,
    shallowReadonly
} from 'vue'
import { initDecorator, toEmitMap } from './helpers'

const EXCLUDE_SUPER_PROPS = ['data','setup']

function defineRawProxy(proxy: any, key: string, target: any): void {
    Object.defineProperty(proxy, key, {
        get: () => target[key],
        set: (value) => {
        target[key] = value
        },
        enumerable: true,
        configurable: true,
    })
}

function defineRefProxy(proxy: any, key: string, target: any): void {
    Object.defineProperty(proxy, key, {
        get: () => target[key].value,
        set: (value) => {
        target[key].value = value
        },
        enumerable: true,
        configurable: true,
    })
}

function getSuper(Ctor: typeof VueImpl): typeof VueImpl | undefined {
    const superProto = Object.getPrototypeOf(Ctor.prototype)
    if (!superProto) {
        return undefined
    }

    return superProto.constructor as typeof VueImpl
}

function getOwn<T extends Object, K extends keyof T>(
    value: T,
    key: K
    ): T[K] | undefined {
    return value.hasOwnProperty(key) ? value[key] : undefined
}

function copyNotExistProps(dest:any,src:any,propNames?:string[]){
    dest = dest || {}
    if(src){
        const destPropNames = Object.getOwnPropertyNames(dest)
        propNames = propNames || Object.getOwnPropertyNames(src)
        if(propNames.length > 0){
            propNames.forEach(item=>{
                if(destPropNames.indexOf(item) == -1){
                    dest[item] = src[item]
                }
            })
        }
    }
    return dest;
}
function getNotExistProps(src:any,dest:any,propNames?:string[]){
    let result:any
    if(src){
        propNames = propNames || Object.getOwnPropertyNames(src)
        if(propNames.length > 0){
            const isNullDest = isNull(dest);
            result = {}
            propNames.forEach(item=>{
                const srcVal = src[item]
                if(!isNull(srcVal) && (isNullDest || isNull(dest[item]))){
                    result[item] = srcVal
                }
            })
        }
        result = result || {...src}
    }
    return result
}

export type DataPropWrapMode = 'ref' | 'raw' | 'shallowRef' | 'shallowReactive' | 'shallowReadonly';
export interface DataPropOptions{
    wrapMode?:DataPropWrapMode
}

declare const withDefaultSymbol: unique symbol
export type DataFactory=(ctx: OptionsContext)=>()=>any;
export type OptionsFactoryHandle=(ctx: OptionsContext)=>ComponentOptions;
export interface OptionsFactory {
    name : string,
    order : number,
    handle:OptionsFactoryHandle;
}

export interface OptionsContext {
    options : ComponentOptions,
    dataFactory?:DataFactory,
    beforeHandle?:OptionsFactoryHandle,
    afterHandle?:OptionsFactoryHandle,
    readonly classType:VueConstructor,
    excludeKeys:string[],
    excludeMethods:string[],
    excludeProps:string[],
    dataProps:PropertyDescriptorMap,
    dataPropOptions:Record<string,DataPropOptions>,
    addFactory:(factory:OptionsFactory)=>void,
    removeFactory:(name:string)=>void,
    hasFactory:(name:string)=>boolean,
    clearFactories:()=>void,
    getOrderedFactories:()=>OptionsFactory[]
    getAttr:(name:string)=>any,
    setAttr:(name:string,value:any)=>void,
    hasAttr:(name:string)=>boolean,
    removeAttr:(name:string)=>void,
    clearAttrs:()=>void
}


export interface WithDefault<T> {
    [withDefaultSymbol]: T
}
export type VueWithProps<P> = Vue<ExtractProps<P>, {}, ExtractDefaultProps<P>> & ExtractProps<P>
export type ExtractProps<P> = {
    [K in keyof P]: P[K] extends WithDefault<infer T> ? T : P[K]
}
export type ExtractDefaultProps<P> = {
    [K in DefaultKeys<P>]: P[K] extends WithDefault<infer T> ? T : never
}
export type DefaultKeys<P> = {
    [K in keyof P]: P[K] extends WithDefault<any> ? K : never
}[keyof P]

export type DefaultFactory<T> = (
    props: Record<string, unknown>
) => T | null | undefined


export interface PropOptions<T = any, D = T> {
    type?: PropType<T> | true | null
    required?: boolean
    default?: D | DefaultFactory<D> | null | undefined | object
    validator?(value: unknown): boolean
}


export interface VueStatic {
// -- Class component configs

/**
 * @internal
 * The cache of __vccOpts
 */
__c?: ComponentOptions

/**
 * @internal
 * The base options specified to this class.
 */
__b?: ComponentOptions

/**
 * @internal
 * Component options specified with `@Options` decorator
 */
__o?: ComponentOptions

/**
 * @internal
 * Decorators applied to this class.
 */
__d?: ((options: ComponentOptions) => void)[]

/**
 * @internal
 * Registered (lifecycle) hooks that will be ported from class methods
 * into component options.
 */
__h: string[]

/**
 * @internal
 * Final component options object that Vue core processes.
 * The name must be __vccOpts since it is the contract with the Vue core.
 */
__vccOpts: ComponentOptions

// --- Vue Loader etc injections

/** @internal */
render?: () => VNode | void

/** @internal */
ssrRender?: () => void

/** @internal */
__file?: string

/** @internal */
__cssModules?: Record<string, any>

/** @internal */
__scopeId?: string

/** @internal */
__hmrId?: string
}

export type PublicProps = VNodeProps &
AllowedComponentProps &
ComponentCustomProps

export type VueBase = Vue<unknown, never[]>

export type VueMixin<V extends VueBase = VueBase> = VueStatic & {
prototype: V
}

export interface ClassComponentHooks {
// To be extended on user land

data?(): object
beforeCreate?(): void
created?(): void
beforeMount?(): void
mounted?(): void
beforeUnmount?(): void
unmounted?(): void
beforeUpdate?(): void
updated?(): void
activated?(): void
deactivated?(): void
render?(): VNode | void
errorCaptured?(err: Error, vm: Vue, info: string): boolean | undefined
serverPrefetch?(): Promise<unknown>
}

export type Vue<
Props = unknown,
Emits extends EmitsOptions = {},
DefaultProps = {}
> = ComponentPublicInstance<
Props,
{},
{},
{},
{},
Emits,
PublicProps,
DefaultProps,
true
> &
ClassComponentHooks

export interface VueConstructor<V extends VueBase = Vue> extends VueMixin<V> {
new (...args: any[]): V

// --- Public APIs
storeType:'vuex'|'pinia';
defaultStore:any;
registerHooks(keys: string[]): void

with<P extends { new (): unknown }>(
    Props: P
): VueConstructor<V & VueWithProps<InstanceType<P>>>
}


const defaultBeforeFactoryHandle:OptionsFactoryHandle=function(ctx:OptionsContext){
    const options = ctx.options
    const Ctor = ctx.classType
    const proto = Ctor.prototype
    const propNames = Object.getOwnPropertyNames(proto)
    // Inject base options as a mixin
    const base = getOwn(Ctor, '__b')
    if (base) {
        options.mixins = options.mixins || []
        options.mixins.unshift(base)
    }

    options.methods = { ...options.methods }
    options.computed = { ...options.computed }


    propNames.forEach((key) => {
        if (key === 'constructor') {
            return
        }

        // hooks
        if (Ctor.__h.indexOf(key) > -1) {
            ;(options as any)[key] = (proto as any)[key]
            return
        }

        const descriptor = Object.getOwnPropertyDescriptor(proto, key)!

        if(key === 'setup'){
            options.setup = descriptor.value
            return;
        }

        // methods
        if (typeof descriptor.value === 'function') {
            ;(options.methods as any)[key] = descriptor.value
            return
        }

        // computed properties
        if (descriptor.get || descriptor.set) {
            ;(options.computed as any)[key] = {
            get: descriptor.get,
            set: descriptor.set,
            }
            return
        }
    })

    // from Vue Loader
    const injections = [
        'render',
        'ssrRender',
        '__file',
        '__cssModules',
        '__scopeId',
        '__hmrId',
    ]
    injections.forEach((key) => {
        if ((Ctor as any)[key]) {
            options[key] = (Ctor as any)[key]
        }
    })

    // Handle super class options
    const Super = getSuper(Ctor)
    if (Super) {
        const superOptions = Super.__vccOpts
        const extendOptions : ComponentOptions = getNotExistProps(superOptions,options,Ctor.__h);
        if(!options.components){
            options.components = {};
        }
        options.components = copyNotExistProps(options.components,superOptions.components)
        options.computed = copyNotExistProps(options.computed,superOptions.computed)
        options.methods = copyNotExistProps(options.methods,superOptions.methods)
        options.watch = copyNotExistProps(options.watch,superOptions.watch)
        options.emits = copyNotExistProps(toEmitMap(options.emits),toEmitMap(superOptions.emits))
        EXCLUDE_SUPER_PROPS.forEach(item=>{
            delete extendOptions[item]
        })
        Object.assign(options,extendOptions)
        //options.extends = extendOptions
    }

    return options
}
const defaultDataFactory:DataFactory=function(ctx:OptionsContext){
    const result = function(){
        const Ctor = ctx.classType
        const excludeProps = ctx.excludeProps
        const data:any = new Ctor()
        const plainData: any = {}
        const dataKeys = Object.keys(data)

        dataKeys.forEach((key) => {
            let val;
            if (excludeProps.indexOf(key) > -1 || ((val = data[key]) && val.__s)) {
                return
            }
            const dataOption:DataPropOptions =  ctx.dataPropOptions[key];
            const wrapMode:DataPropWrapMode = dataOption && dataOption.wrapMode ? dataOption.wrapMode : 'ref';
            
            switch(wrapMode){
                case 'ref':
                    plainData[key] = ref(val);
                    defineRefProxy(data, key, plainData);
                    break;
                case 'raw':
                    plainData[key] = val;
                    defineRawProxy(data, key, plainData);
                    break;
                case 'shallowRef':
                    plainData[key] = shallowRef(val);
                    defineRefProxy(data, key, plainData);
                    break;
                case 'shallowReactive':
                    plainData[key] = shallowReactive(val);
                    defineRawProxy(data, key, plainData);
                    break;
                case 'shallowReadonly':
                    plainData[key] = shallowReadonly(val);
                    defineRawProxy(data, key, plainData);
                    break;
                default:
                    plainData[key] = ref(val);
                    defineRefProxy(data, key, plainData);
                    break;
            }
        })
        Object.defineProperties(plainData,ctx.dataProps);
        return plainData
    }
    return result
}
const defaultAfterFactoryHandle:OptionsFactoryHandle=function(ctx:OptionsContext){
    const options = ctx.options

    const excludeKeys:string[] = [...ctx.excludeKeys]
    if(options.computed){
        excludeKeys.push(...Object.keys(options.computed))
    }
    if(options.props){
        excludeKeys.push(...Object.keys(options.props))
    }
    const excludeMethods:string[] = [...excludeKeys,...ctx.excludeMethods]
    excludeMethods.forEach((key)=>{
        delete options.methods[key]
    })

    if(options.methods){
        excludeKeys.push(...Object.keys(options.methods))
    }

    const excludeProps:string[] = [...excludeKeys,...ctx.excludeProps]

    ctx.excludeProps = excludeProps

    return options
}


class OptionsContextImpl implements OptionsContext{
    options !: ComponentOptions
    readonly classType!:VueConstructor
    dataFactory?: DataFactory
    beforeHandle?:OptionsFactoryHandle
    afterHandle?:OptionsFactoryHandle
    attrs:Map<string,any> = new Map<string,any>()
    factoryMap:Record<string,OptionsFactory>={}
    excludeKeys:string[]=[]
    excludeMethods:string[]=[]
    excludeProps:string[]=[]
    dataProps: PropertyDescriptorMap = {}
    dataPropOptions:Record<string,DataPropOptions> = {}
    constructor(options : ComponentOptions,target:VueConstructor){
        this.options = options
        this.classType = target
        this.dataFactory = defaultDataFactory
        this.beforeHandle = defaultBeforeFactoryHandle
        this.afterHandle = defaultAfterFactoryHandle
    }
    addFactory(factory:OptionsFactory){
        this.factoryMap[factory.name]=factory
    }
    removeFactory(name:string){
        delete this.factoryMap[name]
    }
    hasFactory(name: string){
        return Reflect.has(this.factoryMap,name)
    }
    getOrderedFactories(){
        let list = Object.values(this.factoryMap)
        list = list.sort((o1,o2)=>{
            return o1.order - o2.order
        })
        return list
    }

    clearFactories(){
        const keys =Object.keys(this.factoryMap)
        keys.forEach(key=>{
            delete this.factoryMap[key]
        })
    }

    getAttr(name:string){
        return this.attrs.get(name)
    }
    hasAttr(name:string){
        return this.attrs.has(name)
    }
    setAttr(name:string,value:any){
        this.attrs.set(name,value)
    }
    removeAttr(name:string){
        this.attrs.delete(name)
    }
    clearAttrs(){
        this.attrs.clear()
    }
}


class VueImpl {
    static __h = [
        'beforeCreate',
        'created',
        'beforeMount',
        'mounted',
        'beforeUnmount',
        'unmounted',
        'beforeUpdate',
        'updated',
        'activated',
        'deactivated',
        'render',
        'errorCaptured',
        'serverPrefetch',
    ]
    static storeType:'vuex'|'pinia'='vuex';
    static defaultStore:any;

    static get __vccOpts(): ComponentOptions {
        // Early return if `this` is base class as it does not have any options
        if (this === Vue) {
            return {}
        }

        const Ctor = this as VueConstructor

        const cache = getOwn(Ctor, '__c')
        if (cache) {
            return cache
        }

        initDecorator(Ctor);

        // If the options are provided via decorator use it as a base
        let options: ComponentOptions = { ...getOwn(Ctor, '__o') }
        
        const optionsContext : OptionsContext = new OptionsContextImpl(options,Ctor)

        const decorators = getOwn(Ctor, '__d')
        if (decorators) {
            decorators.forEach((fn) => fn(optionsContext))
        }

        if(optionsContext.beforeHandle){
            options = optionsContext.beforeHandle(optionsContext)
            optionsContext.options = options
        }


        const factories:OptionsFactory[]=optionsContext.getOrderedFactories()
        for(let i=0;i<factories.length;i++){
            const item = factories[i]
            options = item.handle(optionsContext)
            optionsContext.options = options
        }

        if(optionsContext.afterHandle){
            options = optionsContext.afterHandle(optionsContext)
            optionsContext.options = options
        }

        const dataFactory = optionsContext.dataFactory || defaultDataFactory
        options.data = dataFactory(optionsContext)

        if(!options.name){
            options.name = Ctor.name
        }
        Ctor.__c = options
        return options
    }

    static registerHooks(keys: string[]): void {
        keys.forEach(item=>{
            if(this.__h.indexOf(item) == -1){
                this.__h.push(item)
            }
        })
    }

    

    static with(Props: { new (): unknown }): VueConstructor {
        const propsMeta = new Props() as Record<string, Prop<any> | undefined>
        const props: ComponentObjectPropsOptions = {}

        Object.keys(propsMeta).forEach((key) => {
        const meta = propsMeta[key]
        props[key] = meta ?? null
        })

        class PropsMixin extends this {
        static __b: ComponentOptions = {
            props,
        }
        }
        return PropsMixin as VueConstructor
    }


}

export const Vue: VueConstructor = VueImpl as VueConstructor
